<?php

// /////////////////////////////////////////////////////////////////////////////
/**
 * IConnector implementation for PHP Data Objects.
 *
 * System requirements:
 * <ul>
 * <li>PHP 5</li>
 * <li>The {@link PHP_MANUAL#book.pdo PHP Data Objects}</li>
 * </ul>
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * The Connector library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * {@link http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public
 * License}
 * for more details.
 *
 * @author Per Egil Roksvaag
 * @copyright 2009 Per Egil Roksvaag
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public
 *          License
 * @package connector
 * @version 2.0.0
 */

// /////////////////////////////////////////////////////////////////////////////
/**
 * Includes the parent class.
 */

require_once ("BaseConnector.php");

// /////////////////////////////////////////////////////////////////////////////
/**
 * IConnector implementation for the PHP Data Objects.
 *
 * @package connector
 */
class PdoConnector extends BaseConnector implements IConnector {
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Open a PDO connection and set global options.
	 *
	 * @param array $connection
	 *        	An associated array of connection settings, like host and user
	 *        	name.
	 * @param array $options
	 *        	An associated array of global options for the resulting
	 *        	instance.
	 * @return PdoConnector
	 */
	public function PdoConnector($connection, $options = array()) {
		try {
			if ($this->open ( $connection )) {
				switch ($this->conn->getAttribute ( PDO::ATTR_DRIVER_NAME )) {
					case "odbc" :
						break;
					case "mssql" :
						break;
					case "mysql" :
						break;
				}
				parent::BaseConnector ( $options );
			} else if ($this->lookup ( "throwException", $options )) {
				throw new Exception ( "PdoConnector error: Connection failed." );
			} else if ($this->lookup ( self::LOG_ERROR, $options )) {
				$log = "PdoConnector error: Connection failed in ";
				error_log ( $log . $_SERVER ["SCRIPT_FILENAME"] );
			}
		} catch ( PDOException $e ) {
			$log = "PdoConnector error: " . $e->getMessage ();
			error_log ( $log . $_SERVER ["SCRIPT_FILENAME"] );
		}
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Open a database connection.
	 *
	 * @param array $connection
	 *        	An associated array of connection settings, like host and user
	 *        	name.
	 * @return bool <var>true</var>, if a database connection was successfully
	 *         opened, <var>false</var> otherwise.
	 */
	protected function open($connection) {
		$hostname = $this->lookup ( self::CONN_HOSTNAME, $connection );
		$database = $this->lookup ( self::CONN_DATABASE, $connection );
		$username = $this->lookup ( self::CONN_USERNAME, $connection );
		$password = $this->lookup ( self::CONN_PASSWORD, $connection );
		
		$persistent = $this->lookup ( self::CONN_PERSISTENT, $connection, false );
		$native = $this->lookup ( self::CONN_NATIVE, $connection, array () );
		$driver = $this->lookup ( self::CONN_DRIVER, $connection );
		$port = $this->lookup ( self::CONN_PORT, $connection );
		
		if ($driver == "odbc" && $database) {
			$dsn = "{$driver}:{$database}";
		} else {
			$dsn = "{$driver}:";
			$database && $dsn .= "dbname={$database};";
			$hostname && $dsn .= "host={$hostname};";
			$port && $dsn .= "port={$port};";
		}
		
		$native = array ();
		$native [PDO::ATTR_PERSISTENT] = $persistent;
		
		switch ($driver) {
			case "odbc" :
			case "mssql" :
				$this->conn = new PDO ( $dsn, $username, $password );
				break;
			default :
				$this->conn = new PDO ( $dsn, $username, $password, $native );
				break;
		}
		return is_object ( $this->conn );
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Send a SQL SELECT query to the database and get the query result.
	 *
	 * @see IConnector::select().
	 * @param string $query
	 *        	A SQL query to execute on a database.
	 * @param array $param
	 *        	An associated array of values to be used in the $query.
	 * @param array $map
	 *        	An array of type definitions for the <var>$param</var> values.
	 * @param array $options
	 *        	An associated array of options, see the {@tutorial
	 *        	connector.pkg#options.element}.
	 * @throws Exception when $param doesn't match the type definition $map.
	 * @return array The query result as a table (array of associated arrays).
	 */
	public function select($query, $param = array(), $map = array(), $options = array()) {
		$param = TypeValidator::check ( $param, $map );
		$query = $this->bind ( $query, $param, $stack, $options );
		$query = $this->build ( $query, $options );
		$hash = $this->getHash ( $query, $stack, $options );
		
		if ($this->lookup ( self::LOG_DEBUG, $options )) {
			$log1 = "PdoConnector debug: Select query in ";
			$log2 = $stack ? LB . "Params: " . @implode ( ", ", $stack ) : "";
			error_log ( $log1 . $_SERVER ["SCRIPT_FILENAME"] . LB . $query . $log2 );
		}
		if ($this->getCache ( $hash, $table, $options )) {
			return $table;
		}
		if ($this->lookup ( self::PARAM_QUERIES, $options )) {
			$stmt = $this->conn->prepare ( $query );
			$this->bindStatement ( $stmt, $stack, $options );
			$success = $stmt->execute ();
		} else {
			$stmt = $this->conn->query ( $query );
			$success = is_object ( $stmt );
		}
		if ($success) {
			do
				$table [] = $this->fetch ( $stmt, $options ); while ( $stmt->nextRowset () );
			$stmt->closeCursor ();
			
			if (count ( $table ) == 1)
				$table = current ( $table );
			$this->setCache ( $hash, $table, $options );
			return $table;
		}
		if ($this->lookup ( self::LOG_ERROR, $options )) {
			$log1 = "PdoConnector error: Select query in ";
			$log2 = $stack ? LB . "Params: " . @implode ( ", ", $stack ) : "";
			error_log ( $log1 . $_SERVER ["SCRIPT_FILENAME"] . LB . $query . $log2 );
			$errorInfo = $stmt->errorInfo ();
			error_log ( "PdoConnector error: " . end ( $errorInfo ) );
		}
		return false;
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Send a SQL INSERT query to the database and get the IDENTITY ID
	 * generated from the last INSERT operation (if any).
	 *
	 * @see IConnector::insert().
	 * @param string $query
	 *        	A SQL query to execute on a database.
	 * @param array $param
	 *        	An associated array of values to be used in the $query.
	 * @param array $map
	 *        	An array of type definitions for the <var>$param</var> values.
	 * @param array $options
	 *        	An associated array of options, see the {@tutorial
	 *        	connector.pkg#options.element}.
	 * @throws Exception when $param doesn't match the type definition $map.
	 * @return int The IDENTITY ID of the last inserted row.
	 */
	public function insert($query, $param = array(), $map = array(), $options = array()) {
		$param = TypeValidator::check ( $param, $map );
		$query = $this->bind ( $query, $param, $stack, $options );
		$driver = $this->conn->getAttribute ( PDO::ATTR_DRIVER_NAME );
		
		switch ($driver) {
			case "odbc" :
			case "mssql" :
				$query = rtrim ( $query, ";" ) . ";" . LB . "SELECT SCOPE_IDENTITY();";
				break;
		}
		if ($this->lookup ( self::LOG_DEBUG, $options )) {
			$log1 = "PdoConnector debug: Insert query in ";
			$log2 = $stack ? LB . "Params: " . @implode ( ", ", $stack ) : "";
			error_log ( $log1 . $_SERVER ["SCRIPT_FILENAME"] . LB . $query . $log2 );
		}
		if ($this->lookup ( self::PARAM_QUERIES, $options )) {
			$stmt = $this->conn->prepare ( $query );
			$this->bindStatement ( $stmt, $stack, $options );
			$success = $stmt->execute ();
		} else {
			$stmt = $this->conn->query ( $query );
			$success = ( bool ) $stmt;
		}
		if ($success) {
			if (in_array ( $driver, array (
					"mssql",
					"odbc" 
			) )) {
				do
					$row = $stmt->fetch ( PDO::FETCH_NUM ); while ( $stmt->nextRowset () );
				$stmt->closeCursor ();
				
				$key = is_array ( $row ) ? current ( $row ) : null;
				return is_numeric ( $key ) ? ( int ) $key : $key;
			} else {
				$key = $this->conn->lastInsertId ();
				$stmt->closeCursor ();
				return ($key !== 0) ? $key : null;
			}
		}
		if ($this->lookup ( self::LOG_ERROR, $options )) {
			$log1 = "PdoConnector error: Insert query in ";
			$log2 = $stack ? LB . "Params: " . @implode ( ", ", $stack ) : "";
			error_log ( $log1 . $_SERVER ["SCRIPT_FILENAME"] . LB . $query . $log2 );
			$errorInfo = $stmt->errorInfo ();
			error_log ( "PdoConnector error: " . end ( $errorInfo ) );
		}
		return false;
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Send a SQL UPDATE query to the database and get the number of rows
	 * updates by the query.
	 *
	 * @see IConnector::update().
	 * @param string $query
	 *        	A SQL query to execute on a database.
	 * @param array $param
	 *        	An associated array of values to be used in the $query.
	 * @param array $map
	 *        	An array of type definitions for the <var>$param</var> values.
	 * @param array $options
	 *        	An associated array of options, see the {@tutorial
	 *        	connector.pkg#options.element}.
	 * @throws Exception when $param doesn't match the type definition $map.
	 * @return int The number of rows updates by the query.
	 */
	public function update($query, $param = array(), $map = array(), $options = array()) {
		$param = TypeValidator::check ( $param, $map );
		$query = $this->bind ( $query, $param, $stack, $options );
		
		if ($this->lookup ( self::LOG_DEBUG, $options )) {
			$log1 = "PdoConnector debug: Update query in ";
			$log2 = $stack ? LB . "Params: " . @implode ( ", ", $stack ) : "";
			error_log ( $log1 . $_SERVER ["SCRIPT_FILENAME"] . LB . $query . $log2 );
		}
		if ($this->lookup ( self::PARAM_QUERIES, $options )) {
			$stmt = $this->conn->prepare ( $query );
			$this->bindStatement ( $stmt, $stack, $options );
			$success = $stmt->execute ();
		} else {
			$stmt = $this->conn->query ( $query );
			$success = ( bool ) $stmt;
		}
		if ($success) {
			return $stmt->rowCount ();
		}
		if ($this->lookup ( self::LOG_ERROR, $options )) {
			$log1 = "PdoConnector error: Update query in ";
			$log2 = $stack ? LB . "Params: " . @implode ( ", ", $stack ) : "";
			error_log ( $log1 . $_SERVER ["SCRIPT_FILENAME"] . LB . $query . $log2 );
			$errorInfo = $stmt->errorInfo ();
			error_log ( "PdoConnector error: " . end ( $errorInfo ) );
		}
		return false;
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Send a SQL DELETE query to the database and get the number of rows
	 * deleted by the query.
	 *
	 * @see IConnector::delete().
	 * @param string $query
	 *        	A SQL query to execute on a database.
	 * @param array $param
	 *        	An associated array of values to be used in the $query.
	 * @param array $map
	 *        	An array of type definitions for the <var>$param</var> values.
	 * @param array $options
	 *        	An associated array of options, see the {@tutorial
	 *        	connector.pkg#options.element}.
	 * @throws Exception when $param doesn't match the type definition $map.
	 * @return int The number of rows deleted by the query.
	 */
	public function delete($query, $param = array(), $map = array(), $options = array()) {
		$param = TypeValidator::check ( $param, $map );
		$query = $this->bind ( $query, $param, $stack, $options );
		
		if ($this->lookup ( self::LOG_DEBUG, $options )) {
			$log1 = "PdoConnector debug: Delete query in ";
			$log2 = $stack ? LB . "Params: " . @implode ( ", ", $stack ) : "";
			error_log ( $log1 . $_SERVER ["SCRIPT_FILENAME"] . LB . $query . $log2 );
		}
		if ($this->lookup ( self::PARAM_QUERIES, $options )) {
			$stmt = $this->conn->prepare ( $query );
			$this->bindStatement ( $stmt, $stack, $options );
			$success = $stmt->execute ();
		} else {
			$stmt = $this->conn->query ( $query );
			$success = ( bool ) $stmt;
		}
		if ($success) {
			return $stmt->rowCount ();
		}
		if ($this->lookup ( self::LOG_ERROR, $options )) {
			$log1 = "PdoConnector error: Delete query in ";
			$log2 = $stack ? LB . "Params: " . @implode ( ", ", $stack ) : "";
			error_log ( $log1 . $_SERVER ["SCRIPT_FILENAME"] . LB . $query . $log2 );
			$errorInfo = $stmt->errorInfo ();
			error_log ( "PdoConnector error: " . end ( $errorInfo ) );
		}
		return false;
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Begins a transaction on the current connection.
	 * The current transaction includes all statements on the connection that
	 * were executed after the call to transaction() and before any calls
	 * to rollback() or commit().
	 *
	 * @see IConnector::transaction().
	 * @param array $options
	 *        	An associated array of options, see the {@tutorial
	 *        	connector.pkg#options.element}.
	 * @return bool true if the transaction was successfully begun, false
	 *         otherwise.
	 */
	public function transaction($options = array()) {
		if ($this->lookup ( self::LOG_DEBUG, $options )) {
			$log = "PdoConnector debug: Begin transaction in ";
			error_log ( $log . $_SERVER ["SCRIPT_FILENAME"] );
		}
		if ($this->conn->beginTransaction ()) {
			return true;
		} else if ($this->lookup ( self::LOG_ERROR, $options )) {
			$log = "PdoConnector error: Begin transaction in ";
			error_log ( $log . $_SERVER ["SCRIPT_FILENAME"] );
			error_log ( "PdoConnector error: " . sqlsrv_errors () );
		}
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Commits the current transaction on the current connection.
	 * The current transaction includes all statements on the connection that
	 * were executed after the call to transaction() and before any calls
	 * to rollback() or commit().
	 *
	 * @see IConnector::commit().
	 * @param array $options
	 *        	An associated array of options, see the {@tutorial
	 *        	connector.pkg#options.element}.
	 * @return bool true if the transaction was successfully committed, false
	 *         otherwise.
	 */
	public function commit($options = array()) {
		if ($this->lookup ( self::LOG_DEBUG, $options )) {
			$log = "PdoConnector debug: Commit in ";
			error_log ( $log . $_SERVER ["SCRIPT_FILENAME"] );
		}
		if ($this->conn->commit ()) {
			return true;
		} else if ($this->lookup ( self::LOG_ERROR, $options )) {
			$log = "PdoConnector error: Commit in ";
			error_log ( $log . $_SERVER ["SCRIPT_FILENAME"] );
			error_log ( "PdoConnector error: " . sqlsrv_errors () );
		}
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Rolls back the current transaction on the current connection.
	 *
	 * The current transaction includes all statements on the connection that
	 * were executed after the call to transaction() and before any calls
	 * to rollback() or commit().
	 *
	 * @see IConnector::rollback().
	 * @param array $options
	 *        	An associated array of options, see the {@tutorial
	 *        	connector.pkg#options.element}.
	 * @return bool true if the transaction was successfully rolled back, false
	 *         otherwise.
	 */
	public function rollback($options = array()) {
		if ($this->lookup ( self::LOG_DEBUG, $options )) {
			$log = "PdoConnector debug: Rollback in ";
			error_log ( $log . $_SERVER ["SCRIPT_FILENAME"] );
		}
		if ($this->conn->rollBack ()) {
			return true;
		} else if ($this->lookup ( self::LOG_ERROR, $options )) {
			$log = "PdoConnector error: Rollback in ";
			error_log ( $log . $_SERVER ["SCRIPT_FILENAME"] );
			error_log ( "PdoConnector error: " . sqlsrv_errors () );
		}
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Add a TOP x statement to a SQL SELECT query when the
	 * {@link IConnector::RESULT_LENGTH} is set.
	 *
	 * @param string $query
	 *        	A SQL query to execute on a database.
	 * @param array $options
	 *        	An associated array of options.
	 * @return string A modified SQL query.
	 */
	protected function build($query, $options = array()) {
		$driver = $this->conn->getAttribute ( PDO::ATTR_DRIVER_NAME );
		$offset = $this->lookup ( self::RESULT_OFFSET, $options, 0 );
		$length = $this->lookup ( self::RESULT_LENGTH, $options );
		
		switch ($driver) {
			case "odbc" :
			case "mssql" :
				if (is_int ( $length )) {
					$number = $offset + $length;
					$pattern = "/\A(SELECT)([\s]+(ALL|DISTINCT))?\b/i";
					$query = preg_replace ( $pattern, "$0 TOP {$number}", $query );
				}
				break;
			case "mysql" :
				if ($offset || is_int ( $length )) {
					if (is_null ( $length ))
						$length = PHP_INT_MAX;
					$query .= LB . "LIMIT  {$offset}, {$length}";
				}
				break;
		}
		return $query;
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Bind values to a prepared statement using named or numeric parameters.
	 *
	 * @param PDOStatement $stmt
	 *        	The prepared statement.
	 * @param array $stack
	 *        	A numeric or associated array of parameter values.
	 * @param array $options
	 *        	An associated array of options.
	 * @return void
	 */
	protected function bindStatement($stmt, $stack, $options = array()) {
		$named = $this->lookup ( self::PARAM_NAMED, $options );
		$prefix = $this->lookup ( self::PARAM_PREFIX, $options );
		
		foreach ( $stack as $name => $value ) {
			$index = $named ? $prefix . $name : $name + 1;
			
			switch (gettype ( $value )) {
				case "NULL" :
					$stmt->bindValue ( $index, $value, PDO::PARAM_NULL );
					break;
				case "boolean" :
					$stmt->bindValue ( $index, $value, PDO::PARAM_BOOL );
					break;
				case "integer" :
					$stmt->bindValue ( $index, $value, PDO::PARAM_INT );
					break;
				default :
					$stmt->bindValue ( $index, $value );
					break;
			}
		}
	}
	
	// /////////////////////////////////////////////////////////////////////////
	/**
	 * Fetch multiple rows of a query result.
	 *
	 * If the {@link IConnector::RESULT_LENGTH} or {@link
	 * IConnector::RESULT_OFFSET}
	 * options are set, some rows are omitted from the beginning and/or the end
	 * of the query result.
	 * If the {@link IConnector::RESULT_KEY_FIELD} option is set, the
	 * resulting table is an <b>associated</b> array of rows.
	 *
	 * @param resource $stmt
	 *        	A statement resource corresponding to an executed statement.
	 * @param array $options
	 *        	An associated array of options.
	 * @return array The query result as a table (array of associated arrays).
	 */
	protected function fetch($stmt, $options = array()) {
		$key = $this->lookup ( self::RESULT_KEY_FIELD, $options );
		$offset = $this->lookup ( self::RESULT_OFFSET, $options, 0 );
		$length = $this->lookup ( self::RESULT_LENGTH, $options );
		
		$number = is_null ( $length ) ? PHP_INT_MAX : $length + $offset;
		$table = array ();
		$index = 0;
		
		while ( $row = $stmt->fetch ( PDO::FETCH_ASSOC ) ) {
			if ($number <= $index)
				break;
			if ($offset > $index ++)
				continue;
			$row = $this->strDecode ( $row, $options );
			$key ? $table [$row [$key]] = $row : $table [] = $row;
		}
		return $table;
	}
	
	// /////////////////////////////////////////////////////////////////////////
}

?>